jacquardSnapshot

← snapshot

9766 bytes
import Link from "next/link";
import { VerdictPanel } from "@/components/verdict-panel";
import { api } from "@/lib/api";
import { type DiffRow, diffLines, statOf, withElisions } from "@/lib/diff";
import { governing } from "@/lib/paths";
import type { DecisionDto } from "@/lib/types";

export const dynamic = "force-dynamic";

/** Fetch a path's text at a snapshot, or null when it isn't there. */
async function readAt(
  repo: string,
  snapshotId: string | null,
  path: string,
): Promise<string | null> {
  if (!snapshotId) return null;
  try {
    const snap = await api.snapshot(repo, snapshotId);
    const file = snap.files.find((f) => f.path === path);
    if (!file) return null;
    const blob = await api.blob(repo, file.blob);
    return blob.content ?? "";
  } catch {
    return null;
  }
}

export default async function DiffPage({
  params,
  searchParams,
}: {
  params: Promise<{ repo: string }>;
  searchParams: Promise<{ from?: string; into?: string; path?: string }>;
}) {
  const { repo } = await params;
  const q = await searchParams;
  const detail = await api.repo(repo);

  const into = q.into ?? detail.default_ref;
  const from =
    q.from ?? detail.refs.map((r) => r.name).find((n) => n !== into) ?? into;

  const link = (next: Partial<{ from: string; into: string; path: string }>) => {
    const sp = new URLSearchParams();
    sp.set("from", next.from ?? from);
    sp.set("into", next.into ?? into);
    const p = next.path ?? q.path;
    if (p) sp.set("path", p);
    return `/repos/${repo}/diff?${sp.toString()}`;
  };

  // The gate's own answer for exactly this promotion — same call the gate
  // page makes, so the two can never disagree.
  let preview: Awaited<ReturnType<typeof api.promotePreview>> | null = null;
  let previewError: string | null = null;
  try {
    preview = await api.promotePreview(repo, from, into);
  } catch (e) {
    previewError = e instanceof Error ? e.message : String(e);
  }

  const changed = preview?.changed ?? [];
  const selected = q.path && changed.includes(q.path) ? q.path : changed[0];

  const decisions: DecisionDto[] = await api
    .decisions(repo)
    .then((d) => d.decisions)
    .catch(() => []);

  const fromHead = detail.refs.find((r) => r.name === from)?.head ?? null;
  const intoHead = detail.refs.find((r) => r.name === into)?.head ?? null;

  let rows: (DiffRow | null)[] = [];
  let stat = { added: 0, removed: 0 };
  if (selected) {
    const [before, after] = await Promise.all([
      readAt(repo, intoHead, selected),
      readAt(repo, fromHead, selected),
    ]);
    const all = diffLines(before ?? "", after ?? "");
    stat = statOf(all);
    rows = withElisions(all);
  }

  return (
    <div>
      <div className="jac-row-between">
        <div>
          <p className="jac-eyebrow">Blast radius</p>
          <h1 className="jac-h1">Diff</h1>
          <p className="jac-lede">
            What promoting <span className="jac-mono">{from}</span> into{" "}
            <span className="jac-mono">{into}</span> would change, and which
            decisions govern it.
          </p>
        </div>
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
          <div className="jac-seg">
            {detail.refs.map((r) => (
              <Link key={r.name} href={link({ from: r.name })} data-on={r.name === from}>
                {r.name}
              </Link>
            ))}
          </div>
          <span className="jac-small" style={{ alignSelf: "center" }}>
            into <span className="jac-mono">{into}</span>
          </span>
        </div>
      </div>

      {previewError ? (
        <div className="jac-error-box">{previewError}</div>
      ) : preview ? (
        <div style={{ marginBottom: 16 }}>
          <VerdictPanel verdict={preview.verdict} repo={repo} />
          {!preview.fast_forward ? (
            <p className="jac-small" style={{ marginTop: 8 }}>
              Not a fast-forward — promotion would be refused outright, before
              the gate is even consulted.
            </p>
          ) : null}
        </div>
      ) : null}

      {changed.length === 0 ? (
        <div className="jac-empty">No paths differ between these refs.</div>
      ) : (
        <div className="jac-diff">
          {/* ---- the cut: which paths changed, and what governs them ---- */}
          <aside className="jac-diff-rail">
            <div className="jac-panel-title" style={{ padding: "0 2px 8px" }}>
              {changed.length} path{changed.length === 1 ? "" : "s"} in the cut
            </div>
            <ul className="jac-cut">
              {changed.map((path) => {
                const govs = governing(path, decisions);
                const unsettledHere = govs.filter((d) => d.state === "unsettled");
                return (
                  <li key={path}>
                    <Link
                      href={link({ path })}
                      className="jac-cut-item"
                      data-on={path === selected}
                    >
                      <span className="jac-cut-path">{path}</span>
                      {unsettledHere.length > 0 ? (
                        <span className="jac-cut-flag" title="governed by an unsettled decision">
                          ●
                        </span>
                      ) : govs.length > 0 ? (
                        <span
                          className="jac-cut-flag"
                          data-settled="true"
                          title="governed, and settled"
                        >
                          ●
                        </span>
                      ) : (
                        <span className="jac-cut-flag" data-ungoverned="true" title="ungoverned">
                          ○
                        </span>
                      )}
                    </Link>
                  </li>
                );
              })}
            </ul>
            <p className="jac-small" style={{ marginTop: 12, lineHeight: 1.6 }}>
              <span style={{ color: "var(--jac-blocked)" }}>●</span> unsettled
              decision governs this path
              <br />
              <span style={{ color: "var(--jac-ok)" }}>●</span> governed and
              settled
              <br />
              <span style={{ opacity: 0.6 }}>○</span> ungoverned — nothing was
              declared
            </p>
          </aside>

          {/* ---- the cloth: the file, thread by thread ---- */}
          <section className="jac-panel">
            <div className="jac-panel-head">
              <span className="jac-mono" style={{ fontSize: 12.5 }}>
                {selected}
              </span>
              <span className="jac-spacer" />
              <span className="jac-diff-stat">
                <span className="jac-diff-add">+{stat.added}</span>
                <span className="jac-diff-del">−{stat.removed}</span>
              </span>
            </div>

            {/* the shape of the change, as cloth */}
            <div className="jac-diff-strip" aria-hidden="true">
              {rows.map((row, i) =>
                row === null ? null : (
                  <span
                    key={`strip-${String(i)}`}
                    data-kind={row.kind}
                    style={{ flex: 1 }}
                  />
                ),
              )}
            </div>

            {selected ? (
              <div className="jac-diff-body">
                {rows.map((row, i) =>
                  row === null ? (
                    <div key={`gap-${String(i)}`} className="jac-diff-gap">
                      ⋯
                    </div>
                  ) : (
                    <div key={`row-${String(i)}`} className="jac-diff-row" data-kind={row.kind}>
                      <span className="jac-diff-ln">{row.a ?? ""}</span>
                      <span className="jac-diff-ln">{row.b ?? ""}</span>
                      <span className="jac-diff-mark">
                        {row.kind === "add" ? "+" : row.kind === "del" ? "−" : " "}
                      </span>
                      <span className="jac-diff-text">{row.text || " "}</span>
                    </div>
                  ),
                )}
              </div>
            ) : null}

            {selected ? (
              <div className="jac-panel-foot">
                {(() => {
                  const govs = governing(selected, decisions);
                  if (govs.length === 0) {
                    return (
                      <span>
                        Nothing governs this path. The gate will admit it and
                        report that it verified nothing.
                      </span>
                    );
                  }
                  return (
                    <>
                      <span>governed by</span>
                      {govs.map((d) => (
                        <Link
                          key={d.id}
                          href={`/repos/${repo}/decisions/${d.id}`}
                          className={`jac-tag${d.state === "unsettled" ? " jac-tag--warn" : ""}`}
                          style={{ textDecoration: "none" }}
                        >
                          {d.state === "unsettled" ? "⚠ " : ""}
                          {d.title.length > 40 ? `${d.title.slice(0, 40)}…` : d.title}
                        </Link>
                      ))}
                    </>
                  );
                })()}
              </div>
            ) : null}
          </section>
        </div>
      )}

      <p className="jac-small" style={{ marginTop: 20 }}>
        <Link href="/">← let Jackie walk you through this instead</Link>
      </p>
    </div>
  );
}